/*
    TreeViewer - Cross-platform software to draw phylogenetic trees
    Copyright (C) 2023  Giorgio Bianchini, University of Bristol
 
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, version 3.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

using System;
using System.Collections.Generic;
using PhyloTree;
using TreeViewer;
using System.Text.RegularExpressions;
using VectSharp;
using System.Runtime.InteropServices;

namespace a19d9a55507e64dacafc1d5ffcef35f76
{
    /// <summary>
    /// This module is used to transform a node into a polytomy. It is possible to select either a single node, or to specify
    /// a criterion according to which nodes will be transformed into polytomies; this is useful e.g. to collapse nodes with
    /// low support values.
    ///    </summary>
    /// <description>
    /// <![CDATA[
    /// ## Further information
    /// The module picks the children of the selected node and drafts them on the parent node. For example, consider the tree
    /// `((a,(b,(c,d))),e);`:
    /// 
    /// <p align="center">
    /// <img height="300" src="">
    /// </p>
    ///
    /// If the LCA of `b`, `c` and `d` (highlighted in the tree) is selected as the subject of this module, then its two
    /// direct children (`b` and `(c,d)`) are pruned off of it and grafted on its parent, so that they become siblings of
    /// `a`:
    ///
    /// <p align="center">
    /// <img height="300" src="">
    /// </p>
    ///
    /// You can imagine this as if the branch leading to the node that is being polytomised were to be compressed until
    /// it has length 0.
    /// ]]>
    /// </description>

    public static class MyModule
    {
        public const string Name = "Polytomise node";
        public const string HelpText = "Transforms nodes into politomies.";
        public const string Author = "Giorgio Bianchini";
        public static Version Version = new Version("1.0.1");
        public const ModuleTypes ModuleType = ModuleTypes.FurtherTransformation;

        public const string Id = "19d9a555-07e6-4dac-afc1-d5ffcef35f76";

        public static bool Repeatable { get; } = true;
        
        private static string Icon16Base64 = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABqSURBVDhPY6AUMEJpOCgqKvoPZZIHSDWACUqTDSg2ACMMkAEx3kExwKt2YwOQqgfixm3N/iA2QcAEsgWEgQDEB2lGpgkCpr6+PkYQBgIQvxEsiqAJAowwALkEahh5aWIYpAOy/D2AgIEBAK9sIxBWstekAAAAAElFTkSuQmCC";
        private static string Icon24Base64 = "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADESURBVEhLzZRNDgIhDIWLJxu3mngYWPu75zBO4lpvhpRpDAMMKGUiXzIpkAmPtg+EMQbWRFBchHuARQEp5cWG8zSrZ0MxBXtzJJeBq43W+vOPEMWKRuQyaEJfAuioX79iD7gkBVDZRylVbVl3k0N37I73wYbnNIPt43Z44aDGRSIsBdpyfxpna1bA7dylTZNHaloiihF+o1lNphixqk2Rv7xF4f34hr7eoqYuatXkKAOsM9X66haYFHOuaaxPVoC7OQDAG55GbedfodH5AAAAAElFTkSuQmCC";
        private static string Icon32Base64 = "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAERSURBVFhH7ZZLDoIwEIapJ3Otid4F1j7QNYfRRLceDTuFmkltO1Po2JD4JZNSQunfeYGqPPR9P17J4xWAkRZDCqjr+qyH0zDLz2ocY4htDnA8YGLQdZ3SmHtcOOHjeGAWIDpmogJgAwpxD1D8BSQJgKzObewylIIUACotTdOs9fAaZnmICtgcbrgNt8/rHuZerFBO6eFnFT4hZnu8+74BHxGhdT5go9DzyhdjaLtagHfF47Kjj5hA8TL8Oo11VSgE2gPBPJjCpCQEkTbZrOBYnGOQ8cQv/XkZAsUbEf4hMTcyk1QFpnE4loq7fnYZui+kzGVZn2MJli8A+kTMKIr3AY4H2nEUgfQAx41zKJyEVfUGjAG3F+nFSsIAAAAASUVORK5CYII=";

        public static Page GetIcon(double scaling)
        {
            byte[] bytes;

            if (scaling <= 1)
            {

                bytes = Convert.FromBase64String(Icon16Base64);
            }
            else if (scaling <= 1.5)
            {
                bytes = Convert.FromBase64String(Icon24Base64);
            }
            else
            {
                bytes = Convert.FromBase64String(Icon32Base64);
            }

            IntPtr imagePtr = Marshal.AllocHGlobal(bytes.Length);
            Marshal.Copy(bytes, 0, imagePtr, bytes.Length);

            RasterImage icon;

            try
            {
                icon = new VectSharp.MuPDFUtils.RasterImageStream(imagePtr, bytes.Length, MuPDFCore.InputFileTypes.PNG);
            }
            catch (Exception ex)
            {
                throw ex.InnerException;
            }
            finally
            {
                Marshal.FreeHGlobal(imagePtr);
            }

            Page pag = new Page(16, 16);
            pag.Graphics.DrawRasterImage(0, 0, 16, 16, icon);

            return pag;
        }

        public static List<(string, string)> GetParameters(TreeNode tree)
        {
            List<string> leafNames = tree.GetLeafNames();

            return new List<(string, string)>()
            {
                /// <param name="Mode:">
                /// This parameter determines whether a single node is transformed into a polytomy, or whether all nodes matching
                /// a search criterion are transformed into polytomies.
                /// </param>
                ( "Mode:", "ComboBox:0[\"Single node\",\"Attribute match\"]" ),

                ("Attribute match", "Group:6"),
                
                /// <param name="Attribute:">
                /// This parameter determines the attribute that needs to match the search criterion. If the attribute name
                /// entered here does not exist in the tree, the module does nothing.
                /// </param>
                ("Attribute:", "TextBox:"),
                
                /// <param name="Attribute type:">
                /// This parameter should correspond to the correct attribute type for the attribute that needs to match the
                /// search criterion. If the attribute type is incorrect, the module does nothing.
                /// </param>
                ("Attribute type:", "AttributeType:String"),
                
                /// <param name="Value:">
                /// This text box is used to enter the value that needs to be matched.
                /// </param>
                ("Value:", "TextBox:"),
                
                /// <param name="Comparison type: " display="Comparison type (`Number`)">
                /// If the [Attribute type](#attribute-type) of the attribute that is being matched is `Number`,
                /// the module can match attributes that are equal, smaller than or greather than the specified [Value](#value).
                /// </param>
                ("Comparison type: ", "ComboBox:0[\"Equal\", \"Smaller than\", \"Greater than\"]"),
                
                /// <param name="Comparison type:" display="Comparison type (`String`)">
                /// If the [Attribute type](#attribute-type) of the attribute that is being matched is `String`,
                /// this parameter determines how the strings are compared. If the value is `Normal`, the strings need to match
                /// exactly. If the value is `Case insensitive`, the case of the strings does not matter (e.g. `AaBbCc` matches
                /// both `aabbcc` and `AABBCC`). If the value is `Culture-aware`, the comparison takes into account culture-specific
                /// rules of the current display language of the OS (for example, in Hungarian `ddzs` would match `dzsdzs`).
                /// </param>
                ("Comparison type:", "ComboBox:0[\"Normal\", \"Case-insensitive\", \"Culture-aware\", \"Culture-aware, case-insensitive\"]"),
                
                /// <param name="Regex">
                /// If this check box is checked, string matches are performed using a regular expression. This makes it possible to
                /// search for complicated strings.
                /// </param>
                ("Regex", "CheckBox:false"),

                /// <param name="Node:">
                /// This parameter determines the node that will be politomised.
                /// </param>
                ("Node:", "Node:[" + System.Text.Json.JsonSerializer.Serialize(leafNames[0]) +"," + System.Text.Json.JsonSerializer.Serialize(leafNames[^1]) + "]"),

                /// <param name="Apply recursively to all children">
                /// This parameter determines whether all children of the selected node(s) are transformed into polytomies.
                /// </param>
                ("Apply recursively to all children", "CheckBox:false"),
                
                /// <param name="Apply">
                /// Applies the changes to the other parameter values and triggers an update of the tree.
                /// </param>
                ("Apply", "Button:")
            };
        }

        public static bool OnParameterChange(object tree, Dictionary<string, object> previousParameterValues, Dictionary<string, object> currentParameterValues, out Dictionary<string, ControlStatus> controlStatus, out Dictionary<string, object> parametersToChange)
        {
            controlStatus = new Dictionary<string, ControlStatus>();
            parametersToChange = new Dictionary<string, object>() { { "Apply", false } };

            if ((string)currentParameterValues["Attribute type:"] == "String")
            {
                controlStatus.Add("Comparison type:", ControlStatus.Enabled);
                controlStatus.Add("Comparison type: ", ControlStatus.Hidden);
                controlStatus.Add("Regex", ControlStatus.Enabled);
            }
            else if ((string)currentParameterValues["Attribute type:"] == "Number")
            {
                controlStatus.Add("Comparison type:", ControlStatus.Hidden);
                controlStatus.Add("Comparison type: ", ControlStatus.Enabled);
                controlStatus.Add("Regex", ControlStatus.Hidden);
            }

            if ((string)previousParameterValues["Attribute:"] != (string)currentParameterValues["Attribute:"])
            {
                string attributeName = (string)currentParameterValues["Attribute:"];

                string attrType = ((TreeNode)tree).GetAttributeType(attributeName);

                if ((string)previousParameterValues["Attribute type:"] == (string)currentParameterValues["Attribute type:"])
                {
                    if (!string.IsNullOrEmpty(attrType))
                    {
                        parametersToChange.Add("Attribute type:", attrType);

                        if (attrType == "String")
                        {
                            controlStatus["Comparison type:"] = ControlStatus.Enabled;
                            controlStatus["Comparison type: "] = ControlStatus.Hidden;
                            controlStatus["Regex"] = ControlStatus.Enabled;
                        }
                        else if (attrType == "Number")
                        {
                            controlStatus["Comparison type:"] = ControlStatus.Hidden;
                            controlStatus["Comparison type: "] = ControlStatus.Enabled;
                            controlStatus["Regex"] = ControlStatus.Hidden;
                        }
                    }
                }
            }

            if ((int)currentParameterValues["Mode:"] == 0)
            {
                controlStatus["Attribute match"] = ControlStatus.Hidden;
                controlStatus["Attribute:"] = ControlStatus.Hidden;
                controlStatus["Attribute type:"] = ControlStatus.Hidden;
                controlStatus["Value:"] = ControlStatus.Hidden;
                controlStatus["Comparison type: "] = ControlStatus.Hidden;
                controlStatus["Comparison type:"] = ControlStatus.Hidden;
                controlStatus["Regex"] = ControlStatus.Hidden;
                controlStatus["Node:"] = ControlStatus.Enabled;
            }
            else if ((int)currentParameterValues["Mode:"] == 1)
            {
                controlStatus["Attribute match"] = ControlStatus.Enabled;
                controlStatus["Attribute:"] = ControlStatus.Enabled;
                controlStatus["Attribute type:"] = ControlStatus.Enabled;
                controlStatus["Value:"] = ControlStatus.Enabled;
                controlStatus["Node:"] = ControlStatus.Hidden;
            }

            return (bool)currentParameterValues["Apply"] || (previousParameterValues["Node:"] != currentParameterValues["Node:"]) || ((bool)previousParameterValues["Apply recursively to all children"] != (bool)currentParameterValues["Apply recursively to all children"]);
        }

        private static void PolytomiseNode(TreeNode node, bool allChildren)
        {
            if (node.Parent != null && node.Children.Count > 1)
            {
                foreach (TreeNode child in node.Children)
                {
                    child.Length += node.Length;
                    child.Parent = node.Parent;
                    node.Parent.Children.Add(child);

                    if (allChildren)
                    {
                        PolytomiseNode(child, allChildren);
                    }
                }

                node.Children.Clear();
                node.Parent.Children.Remove(node);
            }
            else if (node.Children.Count > 1 && allChildren)
            {
                List<TreeNode> nodes = new List<TreeNode>(node.Children);

                foreach (TreeNode child in nodes)
                {
                    PolytomiseNode(child, allChildren);
                }
            }
        }

        public static void Transform(ref TreeNode tree, Dictionary<string, object> parameterValues, Action<double> progressAction)
        {
            int mode = (int)parameterValues["Mode:"];

            bool allChildren = (bool)parameterValues["Apply recursively to all children"];

            if (mode == 0)
            {
                string[] nodeElements = (string[])parameterValues["Node:"];
                TreeNode node = tree.GetLastCommonAncestor(nodeElements);

                if (node == null)
                {
                    throw new Exception("Could not find the requested node! If you have changed the Name of some nodes, please select the node again!");
                }

                PolytomiseNode(node, allChildren);
            }
            else if (mode == 1)
            {
                List<TreeNode> nodes = tree.GetChildrenRecursive();

                string attributeName = (string)parameterValues["Attribute:"];

                string attrType = (string)parameterValues["Attribute type:"];

                string attrValue = (string)parameterValues["Value:"];

                double numberNeedle = attrType == "Number" ? double.Parse(attrValue) : -1;

                int comparisonType = attrType == "String" ? (int)parameterValues["Comparison type:"] : (int)parameterValues["Comparison type: "];

                bool regex = (bool)parameterValues["Regex"];

                StringComparison comparison = StringComparison.InvariantCulture;
                RegexOptions options = RegexOptions.CultureInvariant;
                switch (comparisonType)
                {
                    case 0:
                        comparison = StringComparison.InvariantCulture;
                        options = RegexOptions.CultureInvariant;
                        break;
                    case 1:
                        comparison = StringComparison.InvariantCultureIgnoreCase;
                        options = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
                        break;
                    case 2:
                        comparison = StringComparison.CurrentCulture;
                        options = RegexOptions.None;
                        break;
                    case 3:
                        comparison = StringComparison.CurrentCultureIgnoreCase;
                        options = RegexOptions.IgnoreCase;
                        break;
                }


                Regex reg = regex ? new Regex(attrValue, options) : null;

                foreach (TreeNode node in nodes)
                {
                    bool matched = false;

                    if (node.Attributes.TryGetValue(attributeName, out object attributeValue))
                    {
                        if (attrType == "String" && attributeValue is string actualValue)
                        {
                            if (regex)
                            {
                                if (reg.IsMatch(actualValue))
                                {
                                    matched = true;
                                }
                            }
                            else
                            {
                                if (actualValue.Contains(attrValue, comparison))
                                {
                                    matched = true;
                                }
                            }
                        }
                        else if (attrType == "Number" && attributeValue is double actualNumber)
                        {
                            switch (comparisonType)
                            {
                                case 0:
                                    if (actualNumber == numberNeedle)
                                    {
                                        matched = true;
                                    }
                                    break;
                                case 1:
                                    if (actualNumber < numberNeedle)
                                    {
                                        matched = true;
                                    }
                                    break;
                                case 2:
                                    if (actualNumber > numberNeedle)
                                    {
                                        matched = true;
                                    }
                                    break;
                            }
                        }
                    }

                    if (matched)
                    {
                        PolytomiseNode(node, allChildren);
                    }
                }
            }
        }
    }
}
